# coding=utf-8
import threading
from threading import Thread, Condition, RLock, Semaphore
from types import MethodType, FunctionType

class EmptyBufferException(Exception):
    def __init__(self, *args):
        super(EmptyBufferException, self).__init__(*args)


class StopException(Exception):
    def __init__(self, *args):
        super(StopException, self).__init__(*args)

# =================================== Buffer ====================================
class Buffer:
    def __init__(self, buffer_size=0):
        """
        Construct a buffer with the specified size
        :param buffer_size: the size of the buffer, zero means the buffer is unlimited.
        """
        self._lock = RLock()
        self._buffer_size = buffer_size
        self._has_data = Condition(self._lock)
        self._has_space = Condition(self._lock)
        self._buffer = []
        self._should_stop = False

    def size(self):
        return self._buffer_size

    def get(self, block=True):
        """
        获取数据。该数据将从本缓冲器中删除。
        :param block: 是否阻塞。True表示当本缓冲器中没有数据时当前线程会被阻塞直到有数据为止，
        False表示有数据就返回该数据，没数据扔出EmptyBufferException。
        :return: 从本缓冲器中获取的一个数据
        """
        with self._lock:
            while len(self._buffer) == 0:
                if self._should_stop:
                    raise StopException()
                if block:
                    self._has_data.wait()
                else:
                    raise EmptyBufferException('Buffer is empty')
            if self._should_stop:
                raise StopException()
            data = self._buffer.pop(0)
            self._has_space.notify_all()
        return data

    def read(self, block=True):
        """
        读取数据。与get()不同在于本方法不会从本缓冲器中删除读到的数据。
        :param block: 是否阻塞。True表示当本缓冲器中没有数据时当前线程会被阻塞直到有数据为止，
        False表示有数据就返回该数据，没数据扔出EmptyBufferException。
        :return: 从本缓冲器中读取的一个数据
        """
        with self._lock:
            while len(self._buffer) == 0:
                if self._should_stop:
                    raise StopException()
                if block:
                    self._has_data.wait()
                else:
                    raise EmptyBufferException('Buffer is empty')
            if self._should_stop:
                raise StopException()
            data = self._buffer.get(0)
        return data


    def empty(self):
        """
        Check to see whether or not this buffer is empty
        :return: True if this buffer is empty, False otherwise.
        """
        with self._has_data:
            return len(self._buffer) == 0

    def full(self):
        """
        Check to see whether or not this buffer is full
        :return: True if the size of this buffer is greater than zero and it is full, False otherwise.
        """
        with self._has_data:
            return 0 < self._buffer_size == len(self._buffer)

    def get_all(self, empty_ok=True):
        """
        Get and remove all data from this buffer. This method is blocked only
        if the empty_ok is False and there is no data in the buffer.
        :param empty_ok: True means the method returns an empty list ([]) if this buffer is empty. False
                         means the method will be blocked until there is a data in this buffer.
                         The default value is True.
        :return: a list of all the data of this buffer.
        """
        with self._lock:
            if not empty_ok:
                while len(self._buffer) == 0:
                    if self._should_stop:
                        raise StopException()
                    self._has_data.wait()
            if self._should_stop:
                raise StopException()
            result = self._buffer.copy()
            self._buffer.clear()
            self._has_space.notify_all()
            return result

    def read_all(self, empty_ok=True):
        with self._lock:
            if not empty_ok:
                while len(self._buffer) == 0:
                    if self._should_stop:
                        raise StopException()
                    self._has_data.wait()
            if self._should_stop:
                raise StopException()
            return self._buffer.copy()

    def snapshot(self):
        """
        Get a snapshot of this buffer, same to call read_all(True)
        :return:
        """
        with self._lock:
            return self._buffer.copy()

    def put(self, data, block=True):
        """
        Put the data into this buffer.
        :param data: the data to be put into this buffer
        :param block: True means to block the current thread if there is no space. False means that
        the current thread doesn't block itself and returns from this method immediately
        if there is no space to store the data.
        :return: True if the buffer puts, successfully, the data into this buffer, False otherwise
        """
        with self._lock:
            while len(self._buffer) >= self._buffer_size > 0:
                if self._should_stop:
                    raise StopException()
                if block:
                    self._has_space.wait()
                else:
                    return False
            if self._should_stop:
                raise StopException()
            self._buffer.append(data)
            self._has_data.notify_all()
        return True

    def start(self):
        with self._lock:
            self._should_stop = False

    def stop(self):
        with self._lock:
            self._should_stop = True
            self._has_space.notify_all()
            self._has_data.notify_all()
            self._buffer.clear()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()

    def __enter__(self):
        pass

    @property
    def is_stopped(self):
        with self._lock:
            return self._should_stop

    @property
    def size(self):
        return self._buffer_size

    def __repr__(self):
        with self._lock:
            return str([type(e) for e in self._buffer])


def _check(m):
    if isinstance(m, MethodType) or isinstance(m, FunctionType):
        return
    else:
        raise Exception('Expect a method or function or a list/tuple, other than %s' % m)


def to_callable_list(p):
    result = []
    if type(p) in (tuple, list):
        for e in p:
            result.append(to_callable(e))
    else:
        _check(p)
        result.append((p, []))
    return result


def to_callable(e):
    if type(e) in (tuple, list):
        _check(e[0])
        return e
    else:
        _check(e)
        return (e, [])


# =================================== AutoBuffer ====================================
class AutoBuffer(Buffer):
    def __init__(self, buffer_size, producers=None, consumers=None, daemon=True):
        super(AutoBuffer, self).__init__(buffer_size)
        if producers is None:
            producers = []
        if consumers is None:
            consumers = []
        self._producer_threads = []
        self._consumer_threads = []
        self._producer_targets = to_callable_list(producers)
        self._consumer_targets = to_callable_list(consumers)
        self.daemon = daemon

    def add_producer(self, *target_and_params):
        for tp in target_and_params:
            self._producer_targets.append(to_callable(tp))

    def add_consumer(self, *target_and_params):
        for tp in target_and_params:
            self._consumer_targets.append(to_callable(tp))

    def start(self):
        super(AutoBuffer, self).start()
        if len(self._producer_targets) > 0:
            for target in self._producer_targets:
                th = Thread(target=self._produce, args=[target], daemon=self.daemon)
                self._producer_threads.append(th)
                th.start()

        if len(self._consumer_targets) > 0:
            for target in self._consumer_targets:
                th = Thread(target=self._consume, args=[target], daemon=self.daemon)
                self._consumer_threads.append(th)
                th.start()

    def stop(self, wait_stop=True):
        super(AutoBuffer, self).stop()
        if wait_stop:
            self.wait_stop()

    def wait_stop(self):
        for p in self._producer_threads:
            p.join()
        for c in self._consumer_threads:
            c.join()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop(exc_type, exc_val, exc_tb)

    def __enter__(self):
        self.start()

    def _produce(self, producer_target):
        while not self._should_stop:
            buffer = producer_target[0](*producer_target[1])
            if buffer is not None:
                self.put(buffer)

    def _consume(self, consumer_target):
        while not self._should_stop:
            buffer = self.get()
            consumer_target[0](buffer, *consumer_target[1])


# =================================== CycleThread ====================================
class CycleThread(Thread):
    def __init__(self, target, args=[], group=None, name=None, daemon=None):
        super(CycleThread, self).__init__(group=group, name=name, daemon=daemon)
        self._my_target = target
        self._my_args = args
        self._my_stopped = False

    def run(self):
        while not self._my_stopped:
            self._my_target(*self._my_args)

    def stop(self, wait_stop=True):
        self._my_stopped = True
        if wait_stop:
            self.join()


# ===========================================================================
class Converter:
    def __init__(self, get_func=None, put_func=None):
        self._thread = threading.Thread(daemon=True, target=self.run)
        self._stop = False
        self._get_func = self.get if get_func is None else get_func
        self._put_func = self.put if put_func is None else put_func

    def run(self):
        while not self._stop:
            value = self._get_func()
        
    def get(self):
        pass

    def put(self, object):
        pass

    def start(self):
        pass

    def stop(self):
        pass

    def pause(self):
        pass

    def resume(self):
        pass


# ===========================================================================
class Dating:
    """
    约会对象，1个女孩可以和指定数量的男孩配对。
    """
    def __init__(self, boys:int=1):
        assert boys > 0
        self.boys = boys
        self._main_buf = Buffer(boys)

    def start(self):
        self._main_buf.start()

    def stop(self):
        self._main_buf.stop()

    def boy_come(self, boy):
        """
        男孩来参加配对，配对成功返回(girl, boys, index)，不成功则等待直到成功。
        :param boy: 来配对的男孩，可以是任何对象或者数据
        :return: 返回一个三元组(girl, boys, index)，girl是与boy配对的girl，boys是所有与该girl配对的男孩，
            index表示boys[index]是boy。
        """
        buf = Buffer(1)
        self._main_buf.put((boy, buf))
        return buf.get()

    def girl_come(self, girl):
        """
        女孩来参加配对，配对成功返回含有self.boys个元素的列表，包含所有与之配对的男孩。不成功则等待直到成功。
        :param girl: 来配对的女孩，可以是任何对象或者数据
        :return: 含有self.boys个元素的列表，包含所有与girl配对的男孩
        """
        boys = []
        bufs = []
        for _ in range(self.boys):
            boy, buf = self._main_buf.get()
            boys.append(boy)
            bufs.append(buf)
        for i, buf in enumerate(bufs):
            buf.put((girl, boys, i))
        return boys

# ===========================================================================
class Meeting:
    """
    聚会对象，指定数量的参加者到齐之后才会进行下一步操作
    """
    def __init__(self, num: int):
        """
        创建一个聚会对象
        :param num: 数量，必须大于1。只有指定数量的参加者到齐后，才能进行下一步操作。
        """
        assert num > 1
        self.num = num

        self._buf = Buffer(num)

        lock = RLock()
        self._team_ok = Condition(lock)
        self._last_member = Condition(lock)
        self._members = 0

    def attend(self, attendee):
        self._buf.put(attendee)

        with self._team_ok:
            self._members += 1
            if self._members < self.num:
                self._team_ok.wait()
                result = self._buf.read_all()
                self._members -= 1
                self._last_member.notify_all()
                return result
            self._team_ok.notify_all()
            while self._members > 1:
                self._last_member.wait()
            self._members -= 1

        return self._buf.get_all()


def test_meeting():
    import numpy as np
    import time
    meeting = Meeting(3)
    class Attendee(Thread):
        def __init__(self, id):
            super(Attendee, self).__init__(daemon=True)
            self.id = id
            self._sem = Semaphore(0)

        def __repr__(self):
            return 'id_%s' % self.id

        def run(self):
            while(True):
                team = meeting.attend(self)
                if team[0] is self:
                    print(team)
                time.sleep(0.2) # np.random.uniform(0.1, 1))

    attendees = [Attendee(id) for id in range(10)]
    for att in attendees:
        att.start()

    time.sleep(10)


if __name__ == '__main__':
    test_meeting()